Skip to content

Codex/vae pyhealth2 - #1358

Open
dalloliogm wants to merge 20 commits into
sunlabuiuc:masterfrom
dalloliogm:codex/vae-pyhealth2
Open

dalloliogm wants to merge 20 commits into
sunlabuiuc:masterfrom
dalloliogm:codex/vae-pyhealth2

Conversation

@dalloliogm

Copy link
Copy Markdown
Contributor

Hello,

Almost one year ago I started contributing to the VAE bounty in PyHealth. Unfortunately, I messed up with Git and became busy with work, and time passed, so I never followed up on the review comments on my previous PR.

This is a new implementation of the bounty for PyHealth 2.0.

Contributor: Giovanni Dall’Olio

Contribution Type: Model

Description: The VAE now supports image inputs and categorical EHR code sequences. I’ve also added or updated three notebooks demonstrating synthetic image generation, ChestXray14 image modeling, and EHR sequence modeling, with explanations of potential applications.

Files to Review:

  • pyhealth/models/vae.py - VAE implementation for image and categorical sequence inputs.
  • tests/core/test_vae.py - model behavior, generation, and Trainer compatibility tests.
  • examples/cxr/vae_cxr_synthetic.ipynb - synthetic image VAE example.
  • examples/cxr/vae_cxr_chestxray14.ipynb - ChestXray14 image example.
  • examples/mortality_prediction/vae_ehr_sequences.ipynb - EHR sequence example.
  • examples/cxr/chextXray_image_generation_VAE.py - updated image VAE example.
  • docs/tutorials.rst and docs/api/models/pyhealth.models.VAE.rst - tutorial links and API documentation.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Test Coverage

Total: 77.45% (24533 / 31676 lines)

Modified file Coverage Lines
pyhealth/autoimprove/__init__.py 100.00% 7 / 7
pyhealth/autoimprove/improver.py 94.59% 507 / 536
pyhealth/autoimprove/proposers.py 93.91% 108 / 115
pyhealth/autoimprove/report.py 95.18% 375 / 394
pyhealth/autoimprove/space.py 97.45% 191 / 196
pyhealth/datasets/__init__.py 84.00% 42 / 50
pyhealth/datasets/chbmit.py 84.09% 111 / 132
pyhealth/datasets/physionet2012.py 96.72% 118 / 122
pyhealth/datasets/splitter.py 96.95% 382 / 394
pyhealth/metrics/__init__.py 100.00% 12 / 12
pyhealth/metrics/survival.py 94.25% 82 / 87
pyhealth/models/__init__.py 100.00% 60 / 60
pyhealth/models/apn.py 97.46% 115 / 118
pyhealth/models/armr.py 98.07% 203 / 207
pyhealth/models/gamenet.py 93.90% 154 / 164
pyhealth/models/heartlang.py 98.58% 209 / 212
pyhealth/models/llm_ehr_encoder.py 91.16% 134 / 147
pyhealth/models/micron.py 83.33% 120 / 144
pyhealth/models/molerec.py 65.93% 240 / 364
pyhealth/models/safedrug.py 85.49% 271 / 317
pyhealth/models/trajsurv.py 95.26% 261 / 274
pyhealth/models/utils.py 92.19% 59 / 64
pyhealth/models/vae.py 88.51% 208 / 235
pyhealth/tasks/__init__.py 95.56% 43 / 45
pyhealth/tasks/chestxray14_binary_classification.py 100.00% 22 / 22
pyhealth/tasks/ehr_text_serialization.py 95.51% 234 / 245
pyhealth/tasks/physionet2012_forecasting.py 95.18% 79 / 83
pyhealth/tasks/ptbxl_heartlang.py 100.00% 42 / 42
pyhealth/tasks/seizure_detection_chbmit.py 90.68% 146 / 161
pyhealth/tasks/survival_mimic4.py 90.62% 58 / 64
pyhealth/tasks/temple_university_EEG_tasks.py 80.49% 132 / 164
pyhealth/trainer.py 77.30% 218 / 282

Workflow run · e1ae2f1

@jhnwu3 jhnwu3 added the status: automatic The PR will go through automatic review process. label Oct 30, 2026
@jhnwu3

jhnwu3 commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot added status: manual Pending maintainer's review and removed status: automatic The PR will go through automatic review process. labels Oct 31, 2026
@dalloliogm

Copy link
Copy Markdown
Contributor Author

If the PR is too large, I can split it into two, so the AI review can take place.

The changes to the VAE class are small. Most of the new lines come from the notebooks included to describe what is a VAE and how it can be used. I could split those into a different PR

@jhnwu3

jhnwu3 commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Yes, please split it if possible.

@jhnwu3

jhnwu3 commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

I think the main thing is the .ipynb notebooks are massive, so just one with a simple examples *.py file and then the follow up notebooks would be much appreciated.

@jhnwu3 jhnwu3 added status: automatic The PR will go through automatic review process. and removed status: manual Pending maintainer's review labels Nov 1, 2026
@dalloliogm

Copy link
Copy Markdown
Contributor Author

Hi, I think the problem was that I forgot to clean up the notebook outputs. I've done that now - can you try reviewing again?

There were a few plots that made the notebooks very big - but they should not have been committed.

@dalloliogm

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

Reviewed commit 3e0ff51

@github-actions github-actions Bot added status: manual Pending maintainer's review and removed status: automatic The PR will go through automatic review process. labels Nov 1, 2026

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

AI Review

suggestion: need change
decision: human review

Thank you for coming back to the VAE bounty and porting it to PyHealth 2.0. Adding categorical EHR sequence support, optional label conditioning for images, and three example notebooks is a useful extension. Line coverage for pyhealth/models/vae.py is 88.29%, above the 80% bar, and total repository coverage is 74.07%, above the 60% bar.

A maintainer will also take a look. The change alters the public contract of the existing VAE: hidden_dim changes meaning and default, latent_dim is new, and the generate signature changes. Existing correct calls can therefore break or behave differently, and a maintainer should decide whether that is acceptable.

To get this merged:

  1. Single-label image datasets. The constructor auto-uses a sole output label as the condition. A dataset with a single multilabel or regression output, for example ChestXray14 multilabel, now raises an error, where the old version ignored labels. Only auto-condition when the label is binary or multiclass, and otherwise ignore it unless condition_key is given explicitly.
  2. generate(num_samples=...) on labeled image datasets. This now raises because of the auto-conditioning. Please keep unconditional generation working, for example by sampling the condition at random or only conditioning when condition_key is passed explicitly.
  3. @​torch.no_grad() removed from generate. The returned images now require grad, so .numpy() and similar calls fail. Please restore the decorator or detach the output. The old device argument has also been dropped, and the second positional argument is now sequence_length. Please keep the signature backward compatible or call the change out clearly.
  4. hidden_dim semantics. Previously hidden_dim was the latent size, with a default of 128. It is now the channel width, with a default of 32. Existing VAE(dataset, hidden_dim=N) calls silently build a different network. Please keep hidden_dim as the latent size, or document the break prominently in the description and docs.
  5. Tests. The COVID19CXRImageGeneration task tests and the learning test were deleted, although that task still ships. Please restore tests for the task and a test that training reduces the reconstruction error. Also add tests for the new error branches and for Trainer.train.

Optional suggestions:

  • In the ChestXray14 notebook, the comment about patient-level splits being streaming subsets looks inaccurate. split_by_patient returns subsets that get_dataloader should accept, so you could use it directly.
  • Unsupported PIL modes such as "P" raise a bare KeyError at the mode lookup in _init_image. A clear ValueError would be friendlier.

Reviewed commit 3e0ff51

@dalloliogm

Copy link
Copy Markdown
Contributor Author

/review

@jhnwu3 jhnwu3 added status: automatic The PR will go through automatic review process. and removed status: manual Pending maintainer's review labels Nov 2, 2026
@dalloliogm

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

Reviewed commit 478feab

@github-actions github-actions Bot added status: manual Pending maintainer's review and removed status: automatic The PR will go through automatic review process. labels Nov 2, 2026

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

AI Review

suggestion: need change
decision: human review

Thank you for this contribution. It extends the PyHealth 2.0 VAE to categorical EHR code sequences and optional label-conditioned image generation, and it adds three example notebooks and docs updates. The new model code looks reasonable. A maintainer will also take a look, because the rewrite of pyhealth/models/vae.py changes the existing VAE's public interface and the format of saved checkpoints.

Coverage: pyhealth/models/vae.py is at 89.05%, above the 80% bar for modified files, and the repository total is 74.11%, above the 60% bar. I did not run the tests or notebooks.

To get this merged:

  1. Keep backward compatibility, or state the break clearly. The old encode() and decode() methods are gone. The attributes encoder, decoder, mu and logvar are renamed, and the ResBlock2D architecture is replaced. State_dicts saved from the previous VAE therefore no longer load. Please restore encode and decode (or add thin wrappers). If the architecture change is intentional, document it in the PR description and the docs as a breaking change.
  2. Image processors without an explicit mode used to work, because the channel count was read from the data. They now raise a ValueError. Please infer the channel count from a sample when mode is None, or document this as an intentional break. Also keep a clear error for to_tensor=False, which the old to_tensor check used to catch.
  3. Restore the test coverage that was removed. The multi-epoch "learns to reconstruct" test is now a single optimizer step. The RGB, gradient-flow and state_dict round-trip tests are gone, so the RGB path used by the COVID classification example is untested. Please add at least an RGB forward-pass test and a state_dict round-trip test.

Optional suggestions:

  • The sequence path always samples the latent, even in eval mode, so validation loss is noisy. Using mu when not self.training would make it deterministic.
  • In the synthetic notebook, torch.manual_seed before an eval-mode forward has no effect, so the remark about seeding can be dropped.
  • The ChestXray14 notebook subclasses the binary task only to add patient_id. That would be better placed in the task itself.

Reviewed commit 478feab

@dalloliogm

Copy link
Copy Markdown
Contributor Author

I just noticed that the image-generation part of this bounty was already covered by PR #1351. Thanks for getting that implementation merged.

This PR builds on that work by extending the VAE to categorical EHR sequences, alongside image inputs. It adds examples for synthetic image generation, conditional image generation with ChestXray14, and EHR sequence modeling, and expands the tests to cover reconstruction, generation, and Trainer compatibility across both input types.

I’ve kept image conditioning opt-in and preserved the existing image VAE behavior and COVID-19 CXR task from #1351. The current CI checks are passing. My aim is to make this a complementary extension to #1351, rather than duplicate its image-generation work.

@dalloliogm

Copy link
Copy Markdown
Contributor Author

/review

@jhnwu3 jhnwu3 added status: automatic The PR will go through automatic review process. and removed status: manual Pending maintainer's review labels Nov 2, 2026
@jhnwu3

jhnwu3 commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

/review

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

Reviewed commit f2973cd

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

Reviewed commit bed99d7

@github-actions github-actions Bot added status: manual Pending maintainer's review and removed status: automatic The PR will go through automatic review process. labels Nov 3, 2026

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous review

AI Review

suggestion: need change
decision: human review

Thank you for this contribution. It extends the PyHealth 2.0 VAE with optional label conditioning for images and a new categorical EHR sequence VAE. It also adds three example notebooks, documentation updates and tests. It keeps the earlier image VAE's module names and state_dict keys, which is a nice touch.

A maintainer will take a look as well. The change modifies pyhealth/trainer.py, a shared base class, so Trainer.inference() now behaves differently for any model that returns mask and reconstruction_logits. That is a core-level change that needs a human decision.

To get this merged:

  1. In VAE.generate() for sequence VAEs, the latent is sampled on the device argument. The latent_to_hidden, sequence_decoder and sequence_output layers live on the model's device. If the two differ, generate(device=...) will fail. Either move the latent to self.device, or document that device must match the model's device. Please add a test for this path.

Coverage: vae.py is at 87.0% and chestxray14_binary_classification.py is at 100%, both above the 80% bar. trainer.py is at 77.2%, just under 80%, and the whole repository is at 75.6%, above the 60% bar. The trainer.py shortfall mostly comes from existing code. Still, the new variable-length branch in inference() is tested, so it would be good to confirm that it is covered.

Optional suggestions:

  • The image forward pass no longer accepts a list of image tensors, as the previous version did. Consider restoring that or mentioning the change in the docs.
  • Trainer.inference() stores full per-token probability arrays for sequence VAEs. With large EHR vocabularies this could use a lot of memory, so a note in the docs may help.
  • In the ChestXray14 notebook, the elif not DATA_AVAILABLE branch is unreachable, because DOWNLOAD_DATA is already not DATA_AVAILABLE. The notebook also starts a roughly 5k-image download without asking, so a clearer warning may help.
  • Conditioning is tested only for output shape. A test showing that different conditions change the output would strengthen it.
  • The synthetic notebook describes training results but has no saved outputs.

Reviewed commit bed99d7

@Logiquo

Logiquo commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

/review

@jhnwu3 jhnwu3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tldr; The task schema that gets sent and attributed to the model should be a better pick than a custom inference_output_mode attribute here.

Comment thread pyhealth/trainer.py
if per_step:
if sequence_reconstruction:
# Sequence reconstruction is not a classification task, but
# inference can still return each sample's unpadded tokens

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to instead of creating a custom "infernece_output_mode" we simply reference the model's output schema here to check that it's a sequence processor here for this change here?

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed commit 8706f9a

Comment thread pyhealth/models/vae.py
mask = tokens != self.pad_token
lengths = mask.sum(dim=1).clamp(min=1).cpu()
embeddings = self.token_embedding(tokens)
packed = pack_padded_sequence(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Zero-width code batches fail in pack_padded_sequence

If every sequence in a batch is empty, the padded token tensor has width 0. The lengths are clamped to 1 at line 367, which exceeds that width, so pack_padded_sequence raises. Fix: pad the token tensor to at least one column (for example with F.pad) before packing, or return a zero loss for an all-empty batch. This is non-blocking because it needs every patient in a batch to have no codes.

Comment thread pyhealth/models/vae.py
start = self.sequence_start.expand(tokens.shape[0], -1, -1)
if tokens.shape[1] > 1:
previous_embeddings = self.token_embedding(tokens[:, :-1])
decoder_inputs = torch.cat((start, previous_embeddings), dim=1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Teacher-forced decoder may ignore the latent (posterior collapse)

The decoder receives the ground-truth previous tokens as inputs, so each token can be predicted from its predecessor alone. The latent only sets the initial GRU hidden state. The KL term can then collapse the posterior, and samples drawn from the prior behave like an unconditioned token language model. Consider feeding the latent at every decoder step, or applying word dropout to the decoder inputs, and say in the docstring that the latent may be weakly used. Non-blocking, since the reconstruction and generation API still work as documented.

Comment thread pyhealth/models/vae.py
)
kl_loss = kl_per_sequence.mean()
loss = reconstruction_loss + kl_loss
token_probabilities = F.softmax(logits, dim=-1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] Full token probabilities are computed on every training forward

F.softmax over the B x L x V logits runs in every forward pass, including training, where only the loss is used. That allocates a large tensor each step. Only Trainer.inference needs y_prob, so compute the probabilities there, or return logits and apply softmax in the inference path. Non-blocking; it costs memory and time but is correct.

samples.append({"image": event["path"], "label": int(event[self.disease])})
samples.append(
{
"patient_id": patient.patient_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] patient_id added to the binary task only

The new patient_id key is added only to ChestXray14BinaryClassification. ChestXray14MultilabelClassification does not emit it, so split_by_patient cannot group patients for that task. Task outputs are also cached by the task's vars(), so caches written before this change will not gain the key until they are regenerated. Add the key to the multilabel task too, or note the cache behavior in the change description. Non-blocking, since the notebook only uses the binary task.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review

suggestion: approve
decision: human review

Thank you for this contribution, and for coming back to the VAE bounty. It extends the PyHealth 2.0 VAE with a categorical EHR code-sequence path (GRU encoder, autoregressive decoder and generation) and optional label conditioning for image VAEs. It also adds three example notebooks and the matching docs updates. The image API is kept, including encode/decode, hidden_dim and the state_dict keys.

The diff looks correct and complete to me, and it matches the description. Tests in tests/core/test_vae.py cover image and sequence forward passes, generation, conditioning errors, Trainer evaluate and inference, and a state_dict round trip. Coverage for pyhealth/models/vae.py is 88.5%, above the 80% bar. The repository total is 76.98%, above 60%. pyhealth/trainer.py is at 77.3%, slightly under 80%, but that file is mostly pre-existing code and your added branch is exercised by the tests.

A maintainer will take a look before this merges. The change adds a new branch to Trainer.inference in pyhealth/trainer.py, which is shared core code. The branch only runs for models that set the new inference_output_mode attribute, so existing models should behave the same. It still deserves a human check.

Optional suggestions:

  • A batch where every code sequence is empty would fail in pack_padded_sequence. A guard would avoid that.
  • The teacher-forced decoder can ignore the latent (posterior collapse). KL annealing or input dropout could help later.
  • Trainer.train on a sequence model isn't covered by a test. The notebook does it, but the notebooks aren't run in CI.
  • patient_id was added only to the binary ChestXray14 task. The multilabel task may want it too.

Reviewed commit 8706f9a

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: manual Pending maintainer's review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants